type StateFn = (c: string) => StateFn | void;

export abstract class Parser {
  public isFinished: boolean = false;
  protected abstract state: StateFn;

  receive(string: string) {
    for (let i = 0; i < string.length; i++) {
      this.receiveChar(string.charAt(i));
    }

    this.isFinished = true;
  }

  receiveChar(char: string) {
    const newState = this.state(char);

    if (newState == null) {
      throw new Error('Unexpected token: ' + char);
    }

    this.state = newState;
  }
}

export class ResponseParser extends Parser {
  protected state = this.WAITING_STATUS_LINE;

  private statusLine: string = '';

  private headers: Record<string, unknown> = {};

  private headerName: string = '';
  private headerValue: string = '';

  private bodyParser: null | ChunkedBodyParser = null;

  get response() {
    this.statusLine.match(/HTTP\/1.1 (\d+) ([\s\S]+)/);

    return {
      statusCode: RegExp.$1,
      statusText: RegExp.$2,
      headers: this.headers,
      body: this.bodyParser?.content.join('') ?? '',
    };
  }

  private WAITING_STATUS_LINE(char: string) {
    switch (char) {
      case '\r': {
        return this.WAITING_STATUS_LINE_END;
      }

      default: {
        this.statusLine += char;
        return this.WAITING_STATUS_LINE;
      }
    }
  }

  private WAITING_STATUS_LINE_END(char: string) {
    switch (char) {
      case '\n': {
        return this.WAITING_HEADER_NAME;
      }
    }
  }

  private WAITING_HEADER_NAME(char: string) {
    switch (char) {
      case ':': {
        return this.WAITING_HEADER_SPACE;
      }

      case '\r': {
        if (this.headers['Transfer-Encoding'] === 'chunked') {
          this.bodyParser = new ChunkedBodyParser();
        }

        return this.WAITING_HEADER_BLOCK_END;
      }

      default: {
        this.headerName += char;
        return this.WAITING_HEADER_NAME;
      }
    }
  }

  private WAITING_HEADER_SPACE(char: string) {
    switch (char) {
      case ' ': {
        return this.WAITING_HEADER_VALUE;
      }
    }
  }

  private WAITING_HEADER_VALUE(char: string) {
    switch (char) {
      case '\r': {
        this.headers[this.headerName] = this.headerValue;
        this.headerName = '';
        this.headerValue = '';
        return this.WAITING_HEADER_LINE_END;
      }

      default: {
        this.headerValue += char;
        return this.WAITING_HEADER_VALUE;
      }
    }
  }

  private WAITING_HEADER_LINE_END(char: string) {
    switch (char) {
      case '\n': {
        return this.WAITING_HEADER_NAME;
      }
    }
  }

  private WAITING_HEADER_BLOCK_END(char: string) {
    switch (char) {
      case '\n': {
        return this.WAITING_BODY;
      }
    }
  }

  private WAITING_BODY(char: string) {
    this.bodyParser?.receiveChar(char);
    return this.WAITING_BODY;
  }
}

class ChunkedBodyParser extends Parser {
  protected state: StateFn = this.WAITING_LENGTH;

  private length: number = 0;
  public content: string[] = [];

  private WAITING_LENGTH(char: string) {
    switch (char) {
      case '\r': {
        if (this.length === 0) {
          this.isFinished = true;
        }

        return this.WAITING_LENGTH_LINE_END;
      }

      default: {
        this.length *= 16;
        this.length += parseInt(char, 16);
        return this.WAITING_LENGTH;
      }
    }
  }

  private WAITING_LENGTH_LINE_END(char: string) {
    switch (char) {
      case '\n': {
        return this.READING_TRUNK;
      }
    }
  }

  private READING_TRUNK(char: string) {
    // consume '\r' when the length of last trunk is 0
    if (this.isFinished && this.length === 0) {
      return this.WAITING_NEW_LINE(char);
    }

    this.content.push(char);
    this.length--;

    if (this.length === 0) {
      return this.WAITING_NEW_LINE;
    }

    return this.READING_TRUNK;
  }

  private WAITING_NEW_LINE(char: string) {
    switch (char) {
      case '\r': {
        return this.WAITING_NEW_LINE_END;
      }
    }
  }

  private WAITING_NEW_LINE_END(char: string) {
    switch (char) {
      case '\n': {
        return this.WAITING_LENGTH;
      }
    }
  }
}
